/*
 * Copyright (C) 2013 Will Dignazio <wdignazio@gmail.com>
 */

#include "internal.h"
#include <pthread.h>
#include <stdarg.h>
#include <stdlib.h>

struct QueueElement
{
	WorkItem* data;
	QueueElement* next;
	QueueElement* previous;
};

static inline void
queue_lock(Queue* queue)
{
	if((queue->flags & QUEUE_NOLOCK) == 0)
		pthread_mutex_lock(&queue->lock);
}

static inline void
queue_unlock(Queue* queue)
{
	if((queue->flags & QUEUE_NOLOCK) == 0)
		pthread_mutex_unlock(&queue->lock);
}

static int
queue_free_elem(QueueElement* elem)
{
	if(elem == NULL) 
		return QUEUE_FAIL;
	
	free(elem);
	return QUEUE_SUCCESS;
}

int
queue_free(Queue *queue)
{
	QueueElement *next;
	QueueElement *cur;

	if(queue == NULL)
		return QUEUE_FAIL;

	cur = queue->head;
	next = cur;

	while(cur->next != NULL)
	{
		next = cur->next;
		free(cur);
		cur = next;
	}

	free(queue);
	return QUEUE_SUCCESS;
}

Queue*
queue_create(uint8_t flags)
{
	Queue* queue;
	int ret;

	queue = calloc(1, sizeof(Queue));
	if(queue == NULL) {
		perror("calloc queue");
		return NULL;
	}

	queue->size = 0;
	queue->flags = flags;
	
	ret = pthread_cond_init(&queue->signal, NULL);
	if(ret != 0)
		goto fail;
	
	ret = pthread_mutex_init(&queue->lock, NULL);
	if(ret != 0)
		goto fail;

	if(flags & QUEUE_HEADED) {
		QueueElement* element; 
		
		element = calloc(1, sizeof(QueueElement));
		if(element == NULL) {
			perror("calloc element");
			goto fail;
		}

		if(flags & QUEUE_CIRCULAR)
			element->next = queue->head; // Redunant, but the definition
		
		queue->size = 1;
		queue->head = element;
		queue->tail = element;
	} else { 
		/* 
		 * We're good, just make sure that the head is NULL.
		 * Our operating system might be a derp.
		 */
		queue->head = NULL;
		queue->tail = NULL;
		queue->size = 0;
	}

	return queue;

fail:
	free(queue);
	return NULL;
}

int
queue_push(Queue* queue, WorkItem* elemptr)
{
	QueueElement* qelem;

	if(queue->flags & QUEUE_RO)
		return QUEUE_ISRO;
	
	qelem = malloc(sizeof(QueueElement));
	if(qelem == NULL)
		return QUEUE_FAIL;

	qelem->data = elemptr;

	/*
	 * Might not be used in a threaded environment, 
	 * if that is the case then don't wait on lock.
	 */
	queue_lock(queue);

	if(queue->size == 0) { 
		queue->head = qelem;
		qelem->next = queue->head;
		qelem->previous = queue->head;
	}
	else {
		queue->tail->next = qelem;
		qelem->previous = queue->tail;
	}

	/* Assign new tail to the queue */
	queue->tail = qelem;

	/* 
	 * Fulfill flag that if the queue is circituitous that 
	 * the last element wraps onto the first.
	 */
	if(queue->flags & QUEUE_CIRCULAR) {
		qelem->next = queue->head;
		queue->head->previous = queue->tail;
	}
	else {
		qelem->next = NULL;
	}

	queue->size++;
	/* Unlock the queue if necessary */
	queue_unlock(queue);

	/* 
	 * Broadcast or signal out the new insertion into the queue 
	 * unless the QUEUE_NOSIGNAL flag is set. 
	 */
	if((queue->flags & QUEUE_NOSIGNAL) == 0) {
		if(queue->flags & QUEUE_BROADCAST)
			pthread_cond_broadcast(&queue->signal);
		else
			pthread_cond_signal(&queue->signal);
	}

	return QUEUE_SUCCESS;
}

pthread_cond_t*
queue_getsignal(Queue *queue)
{
	return &queue->signal;
}

int
queue_wait(Queue *queue)
{
	int ret;

	pthread_mutex_lock(&queue->siglock);
	ret = pthread_cond_wait(&queue->signal, &queue->siglock);
	pthread_mutex_unlock(&queue->siglock);

	return ret;
}

int
queue_pop(Queue* queue, WorkItem** ref)
{
	int ret;

	if(queue->flags & QUEUE_RO)
		return QUEUE_ISRO;

	if(queue->size == 0) 
		return QUEUE_EMPTY;

	/* 
	 * Lock up the queue if we're in a thread safe 
	 * environment.
	 */
	queue_lock(queue);
	
	/* Grab the latest insertion into the queue. */
	*ref = queue->tail->data;
	
	if(queue->size == 1 && queue->flags & QUEUE_HEADED) {
		/*
		 * If the headed flag is set, there will always be 
		 * a head to the queue.
		 * 
		 * Instead of freeing, just reuse the memory that's there. 
		 * This could have cons should the user point to specific 
		 * queue elements, but this will be a rather useful performance
		 * increase.
		 */
		queue->head->data = NULL;
		queue->tail = queue->head;

		if(queue->flags & QUEUE_CIRCULAR) { 
			queue->head->previous = queue->head;
			queue->head->next = queue->head;
		}
		else {
			queue->head->previous = NULL;
			queue->head->next = NULL;
		}
	}
	else {
		QueueElement* t_elem;
		t_elem = queue->tail;
		
		/* splice out the tail */
		if(queue->size > 1) {
			queue->tail->previous->next = queue->tail->next; // Will preserve circular
			queue->tail = queue->tail->previous;
			
			if(queue->flags & QUEUE_CIRCULAR)
				queue->head->previous = queue->tail;
		}
		else {
			queue->head = NULL;
			queue->tail = NULL;
		}

	       	ret = queue_free_elem(t_elem); // Free the previous tail
		if(ret != QUEUE_SUCCESS) {
			ret = QUEUE_INTERNAL;
			queue->size--;
			goto done;
		}

		queue->size--;	// Don't forget to decrement the size of the queue
	}

	ret = QUEUE_SUCCESS;
done:
	queue_unlock(queue);
	return ret;
}

int
queue_peek(Queue* queue, WorkItem** ref)
{
	if(queue->size == 0)
		return QUEUE_EMPTY;

	queue_lock(queue);
	*ref = queue->tail->data;
	queue_unlock(queue);

	return QUEUE_SUCCESS;
}


int
queue_empty(Queue* queue)
{
	int ret;
	queue_lock(queue);
	ret = (queue->size == 0);
	queue_unlock(queue);
	return ret;
}

uint64_t
queue_size(Queue* queue)
{
	uint64_t ret;
	queue_lock(queue);
	ret = queue->size;
	queue_unlock(queue);
	return ret;
}
